Skip to content

feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool - #2122

Merged
tlongwell-block merged 29 commits into
mainfrom
eva/agents-everywhere
Jul 22, 2026
Merged

feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool#2122
tlongwell-block merged 29 commits into
mainfrom
eva/agents-everywhere

Conversation

@tlongwell-block

@tlongwell-block tlongwell-block commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Every agent, every community, the moment your GUI starts

On GUI startup, Desktop spawns an ACP harness per (agent, community) pair — every managed agent, in every community it belongs to. Warm authenticated sockets with narrow mention-scoped subscriptions; lazy LLM pool (the agent's brain wakes on the first message addressed to it, everything arriving during wake is held). Idle cost ~5 MB dirty per harness, zero tokens.

Why: mobile can publish a mention into any community's relay but can't spawn a harness on your desktop. Navigation-gated availability made reachability an accident of where the GUI was looking. This makes it a property of the agent — and kills the cold-mention race by construction (the summoning message is always already captured by a warm socket).

Not a new identity model: one keypair, one persona, one 30177 config per agent. Harnesses start only where the agent is already a member.

Structure (four lanes, merged in order)

  • Lane B — pair-scoped Desktop runtime (ddcbb4321, Mari): process map, PID receipts, logs, session cache, per-pair workdirs re-keyed from pubkey to ManagedAgentRuntimeKey; fail-closed replace-before-spawn seam for restart (receipt removed only after confirmed exit); bounded-concurrency startup; per-pair stop/restart.
  • Lane A — ACP lazy agent pool (eccf18cd7, Wren): opt-in --lazy-pool / BUZZ_ACP_LAZY_POOL (eager remains default); relay auth/subscriptions/presence before any ACP child; exactly-one async wake on accepted flushable work; queued dispatch after Ready; bounded 5s→5m retry; stale/duplicate wake rejection; managed_agent_runtime_lifecycle observer frames (listening/waking/ready/failed); explicit shutdown cleanup.
  • Lane C — frontend pair routing (e29aa1673, Max): pair-scoped runtime state, lifecycle presence in UI, settings per-pair controls.
  • Lane D — test battery (e0ca4dd33, Perci): integration matrix incl. native replacement probe (old PID dead, exactly one live process + one receipt).

Verification

Each lane independently reviewed (line review + delta-confirms + mutation-tested unit tests + live native probes; ledger in the build thread). At the integrated head 0f313d2ed (post merge of main 31090b186): cargo fmt --all --check clean, full buzz-acp suite 541 unit + 9 lifecycle, desktop tauri lib 1484 pass, tsc --noEmit clean, desktop unit 3185 pass, all pre-commit/pre-push hooks green.

Known follow-ups (flagged, not in this PR)

  • Phase B map-absence guarantee must become key-scoped when restore goes per-pair multi-relay in the adoption fast-follow: today the no-live-child guarantee lives in the pubkey-scoped to_start filter (correct while restore is record-level, one relay per record); the multi-relay follow-up must key-scope that filter or add the map check to the replacement helper.
  • Real relay→lazy-child wake smoke test (Lane A disclosed gap) — post-integration/live gate.
  • Some live restore cases deliberately deferred from Lane D to post-integration.
  • File-size ratchet: 5 pre-listed files bumped narrowly for this PR's feature growth (largest +55, pair receipts in storage.rs); all queued to split per the existing list.

Redteam + review fix round (head 060c100a8)

Seven-lane redteam at 0f313d2ed (2 PASS: crypto/lifecycle-spoof defense verified with live probes; real-native-process replacement/poison battery held) plus Wes review produced four fix lanes, all merged and re-gated:

  • restore.rs (F1/F2, Dawn): restore now spawns lazy=true (no idle brains on ordinary launch); Phase B takes a live-map guard mirroring start_pair, so a concurrent reconcile's live child is skipped, never terminated (SpawnOutcome::{Spawned,Skipped,Failed}).
  • runtime authority (C2/C4/C5, Wren): per-spawn generation nonce (BUZZ_MANAGED_AGENT_START_NONCE) carried in every signed lifecycle frame and compared before mutation — replayed genuine frames from a prior harness generation are rejected even while a replacement child is alive (also zeroes the malicious-relay replay ceiling); list_managed_agent_runtimes reaps exited pairs under the transition/store locks and emits explicit Stopped (no stale "Here" on a dead PID); reconcile carries the record updated_at freshness boundary so an intervening user Stop/edit wins; reconcile filters on start_on_app_launch"every agent everywhere" means every auto-start agent everywhere; an explicit opt-out survives. Plus review hygiene: 64-hex pubkey validation/canonicalization in ManagedAgentRuntimeKey, eq_ignore_ascii_case, dead ManagedAgentRuntimeTarget and legacy write_agent_pid_file removed.
  • frontend honesty (C1/C3 + badge, Max): observer lifecycle ingestion opens whenever a managed agent exists (cold all-stopped start included); runtime events invalidate both pair-runtime and managed-agent caches; startup reconcile merges against its baseline instead of clobbering newer lifecycle/user-action rows; ChannelScreen wires the active community relay into MembersSidebar — pair truth when known, legacy Running/Stopped when unknown, no false "Unavailable".
  • live two-relay gate (Perci): opt-in Playwright spec (BUZZ_E2E_AGENTS_EVERYWHERE=1, own config) drives two isolated local relays with the same agent identity: zero ACP subprocesses before mention, idle RSS/process counts recorded, exactly one correctly-isolated ACK per relay, no owned PIDs after teardown. Excluded from ordinary smoke runs.

Gates at the integrated head (same shell as git rev-parse HEAD = 060c100a8, post merge of main 768cd4dbe): cargo fmt --all --check clean; clippy -D warnings for desktop tauri (all targets) and buzz-acp; buzz-acp suite 542; desktop tauri 1486 + 3; tsc --noEmit; desktop unit 3190/3190; production build; smoke acceptance channels.spec.ts:2898 (members sidebar respawn) passed; pnpm check file-size/px/pubkey gates clean; pre-push full hook suite green.

Round-2 fix: agent-wide lifecycle fan-out (head after 060c100a8)

Max's independent review at 060c100a8 found the legacy agent-wide paths never fanned out to pair runtimes. Fixed in a three-commit lane (Wren, verified independently by Max and Eva, accepted at exact tip):

  • Agent-wide Stop/Delete/persona-cascade drain every pair for the pubkey (previously first-key-only — Delete could leave invisible orphan harnesses holding the agent's key). Per-pair teardown failures aggregate instead of short-circuiting; only a runtime whose termination was NOT confirmed is reinserted (marker logging is best-effort after confirmed exit + receipt removal). Explicit pair-scoped stop stays singular by design.
  • Config-change and ACP-install bounces are pair-preserving and lazy: the live pair-key set is snapshotted in the same lock scope as the stop, then exactly that set restarts through the lazy start_pair path (fresh generation nonces, receipts, live-map guard) — never the scalar/eager legacy start. Candidate pre-filters now inspect live pair runtimes instead of the obsolete record.runtime_pid (always None under pair keying — the old pre-check would have silently disabled config-change restarts). The factored pair preflight retains the persona re-snapshot for 30177 relay sync, matching the legacy preflight contract.

Additional named fast-follows from this round:

  • Executable Stop/Delete/bounce seam test: in-tree regressions cover key-selection/restart-target extraction; the full AppHandle-level executable battery needs the same integration harness as the F2 seam test above — same fast-follow, explicitly disclosed.
  • Explicit user Start / create-with-spawn remain scalar-eager: after a drain-all Stop there are no live pairs to snapshot; enumerating desired pairs is reconcile's job. Fast-follow: route Start through the reconcile enumeration.
  • App-shutdown drain still iterates first-key-per-record in-map; terminal receipt/system sweeps catch survivors, so not user-visible. Cleanup follow-up.

Documented limitations (ruled, not fixed here)

  • One-shot reconcile (F3): startup reconcile runs once; membership granted from another device mid-session isn't picked up until relaunch (the explicit bring-an-agent flow covers in-app additions). Fast-follow.
  • Restore-vs-reconcile race test: the F2 skip guard is untestable at unit level without an AppHandle + real process table; it mirrors start_pair's proven guard. A Desktop integration test at the runtime-command seam is a named fast-follow (the two-relay battery honestly can't reach that seam).
  • nsec env footprint (T3): child harnesses receive the agent key via env (pre-existing mechanism); per-pair spawning multiplies the number of same-user processes holding it. Same trust boundary as before; noted because the surface grew Nx.
  • Dual same-build instance (LOW): receipt-layer kill-safety assumes the single-instance plugin boundary; two forced instances of the same build sharing one nest could race each other's harnesses. Hardening path: per-launch owner token.
  • Sticky malformed receipts: truncated receipt JSON is ignored (fail-safe) but never removed; a valid atomic write self-heals the path. Cleanup follow-up.

@tlongwell-block
tlongwell-block requested a review from a team as a code owner July 19, 2026 16:28

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on Wes's behalf after full review in Buzz. The architecture is sound — the lazy-pool state machine, pair receipts, and replace-before-spawn seam are well-built and well-tested — but four concrete issues need to land first:

  1. CI is red. Desktop Core fails desktop-tauri-clippy with 3 errors: ManagedAgentRuntimeTarget is never constructed (dead type in runtime_types.rs — no command takes it), write_agent_pid_file is never used (its last caller was removed; keep it for migration or delete it), and a manual_ignore_case_cmp lint in observer_lifecycle_key (use eq_ignore_ascii_case).
  2. Members-sidebar badge regression. MembersSidebar gained a relayUrl? prop, but ChannelScreen — its only call site — never passes it, so managedAgentRuntime is always undefined and every managed agent now renders a visible "Unavailable" badge regardless of actual state (previously an sr-only status). Wire the prop through, or don't render the fallback badge when runtime state is unknown.
  3. Lane D battery isn't in the diff. Only the fixtures shipped (fake-acp-agent.mjs, twoRelayHarness.ts); nothing imports TwoRelayHarness, so the "integration matrix incl. native replacement probe" described in the PR body isn't in this PR. Either include the specs or state explicitly that they're a follow-up — as merged, this is dead test code.
  4. reconcile_managed_agent_runtimes ignores start_on_app_launch. The launch-restore path filters on it (restore.rs:158), but the new startup reconcile — called unconditionally from AppShell — spawns every local agent in every membership community regardless of that setting. If "every agent, everywhere" deliberately supersedes the setting, say so and reconcile the two paths; right now an agent the user opted out of auto-start gets started anyway.

What's good, for the record: the PoolLifecycle attempt-token design (stale wakes can't clobber a newer pool; abandoned pools are shut down on both the send-fail path and the post-loop drain), fail-closed receipt replacement (receipt removed only after confirmed exit, tested via an injectable seam), a single canonical relay-URL normalizer explicitly kept separate from the NIP-42 AUTH comparison, and documented lock discipline on managed_agent_runtime_transition. Re-review will be quick once the above land.

Reviewed-by: Brain (Buzz agent) on behalf of Wes

@tlongwell-block

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review, Wes. All four items are addressed at the current head 89e77b0df (the CHANGES_REQUESTED stamp predates the fix-lane merges):

  1. Clippy dead code — gone. ManagedAgentRuntimeTarget and write_agent_pid_file no longer exist anywhere in desktop/src-tauri/src (removed in the lifecycle-authority lane, 5e8f65541), and observer_lifecycle_key uses eq_ignore_ascii_case (runtime_commands.rs:55). cargo clippy --workspace --all-targets -- -D warnings is clean locally, and Desktop Core / Rust Lint were green in CI at 72b3bd90b.
  2. MembersSidebar relayUrl — wired through: ChannelScreen.tsx:973 now passes relayUrl={activeCommunity?.relayUrl} (92328bb30, "keep pair runtime presence honest"), so the badge reflects real pair-runtime state instead of always falling back to Unavailable.
  3. Lane D battery — in the diff: desktop/tests/e2e/agents-everywhere.live.spec.ts (added in 726c31d3f) imports TwoRelayHarness and exercises the two-relay matrix, gated behind BUZZ_E2E_AGENTS_EVERYWHERE=1. The fixtures are no longer dead code.
  4. reconcile_managed_agent_runtimes + start_on_app_launch — the reconcile path now filters on record.start_on_app_launch && record.backend == BackendKind::Local (runtime_commands.rs:354), matching the launch-restore path (restore.rs:171). Opted-out agents are no longer auto-started.

Since your review, the branch also picked up two plain merge commits of origin/main (no rebase/force) and a fix keeping a pair runtime tracked when stop fails (72b3bd90b). The latest merge (89e77b0df) resolves a semantic conflict between #2175's new steer_renewal_tests config initializer on main and this PR's lazy_pool config field (one added line in the test helper).

CI was fully green at 72b3bd90b; driving it green again at 89e77b0df now. Ready for re-review.

@matt2e
matt2e force-pushed the eva/agents-everywhere branch 2 times, most recently from ed55dcd to f435d08 Compare July 21, 2026 21:59
npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf and others added 24 commits July 21, 2026 15:05
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
restore_managed_agents_on_launch spawned every auto-start pair with
lazy=false, booting an eager LLM child on every ordinary launch and
silently reintroducing N idle brains — the exact idle cost the pair
runtime is meant to avoid. It also decided "untracked" from receipts
alone, so a startup reconcile that spawned the same pair during the
Phase A window (before restore takes the transition lock) could be
killed and replaced eager, flipping the pair's laziness on a race.

F1: spawn restore children lazy=true, matching startup reconcile and
manual start. Eager on restore buys nothing — a crashed mid-turn
session is not resumed by an eager child; the next mention wakes a
lazy one just as well.

F2: before terminate-and-respawn, check the live process map for an
already-tracked live child at this exact pair key and skip if present,
mirroring start_pair's live-child guard. Leaves a concurrently
reconciled child untouched instead of killing and replacing it.

Live restore/reconcile-race integration coverage (real process table +
AppHandle) is deferred to the Lane D battery; the inlined guard is not
unit-testable in isolation without an AppHandle.

Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Max <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Perci <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Wren <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Pair-scoped stop_managed_agent_runtime removed the runtime from the
map before fallible terminate/wait and bailed with ?, leaving a live
child untracked (and its receipt gone) on failure — the same
invisible-orphan class the agent-wide stop path already handles.
On terminate/wait failure, reinsert the runtime and return the error
so the pair stays visible and stoppable.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Complete the pair-runtime fix plan so each community's UI tells the
truth about its own (agent, community) pair:

- Summary scoping: build_managed_agent_summary resolves the viewed
  workspace's pair key (relay pin, else active workspace relay) and
  looks up status/pid/log and needs_restart by exact pair key — an
  agent running only in another community now reads as stopped here,
  and restart drift is hashed against the pair's own relay instead of
  the currently viewed workspace's, so community switches no longer
  flag spurious restarts.
- Pair-scoped stop: the user-facing stop_managed_agent command stops
  only the active workspace's pair via the new
  stop_managed_agent_workspace_pair (extracted per-pair teardown with
  reinsert-on-failure), clearing only that pair's session cache;
  delete and config-restart flows keep draining every pair through
  stop_managed_agent_process. The auto-restart policy now bounces only
  the drifted pair for the viewed community (safety comment updated).
- Reconcile bootstrap: rename discover_agent_membership to
  probe_agent_relay_access and gate spawn eligibility on the bounded
  authenticated probe succeeding rather than kind-39002 membership
  presence, which cannot exist before a harness first connects and so
  could never bootstrap a new pair; the reconcile also skips
  communities whose relay conflicts with an explicit per-record pin.
- Post-create bootstrap: creating a local agent fires an idempotent
  reconcile across all configured communities so the new agent gets a
  lazy pair everywhere immediately instead of at the next launch; the
  E2E bridge answers reconcile_managed_agent_runtimes with an empty
  set so Playwright runs don't throw.

Tests: pure pair-key resolution (resolve_workspace_pair_key) covering
unpinned per-workspace keys, pin precedence, canonicalization, and
invalid pubkeys; exact-pair stop selection; and reconcile pin
filtering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
matt2e and others added 3 commits July 22, 2026 10:13
Pair-scoped stop_managed_agent_runtime removed the runtime receipt
without any termination attempt when no runtime was tracked at the pair
key. If a valid prior-session receipt still pointed at a live child (the
crash-recovery window for a non-auto-start agent), this stop left the
harness running yet erased the one artifact that sweeps and
terminate_untracked_pair_runtime use to find the orphan, so a follow-up
start would then spawn a duplicate harness for the same pair. That was
asymmetric with the tracked path, which keeps the receipt on disk until
a stop actually succeeds.

Route the untracked case through terminate_untracked_pair_runtime before
removing the receipt: it terminates a live child from a valid receipt and
deletes the receipt only after the child exits, so on failure the receipt
stays on disk — restoring the tracked path's keep-until-success invariant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Tyler <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com>
The per-record relay_url pin predates workspace-scoped runtimes. Honoring
it made pinned agents silently skip fan-out to other communities (the bug
Tyler hit live). Instead of migrating/clearing stored pins, ignore them:

- effective_agent_relay_url() now always returns the workspace relay; the
  stored pin is parsed and persisted untouched, so old records read
  identically and rollback to a pin-honoring build is safe.
- reconcile_managed_agent_runtimes drops the record_allows_relay filter:
  every start_on_app_launch agent fans out to every community.
- Spawn-hash/pair-key docs and tests updated: workspace relay changes trip
  the hash even for pinned records; editing the ignored pin does not.
- Relay URL editor field removed from the agent edit dialog; relayUrl is
  never submitted so the stored value is preserved.

Part of #2122.

Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
@tlongwell-block
tlongwell-block merged commit 61cc738 into main Jul 22, 2026
32 checks passed
@tlongwell-block
tlongwell-block deleted the eva/agents-everywhere branch July 22, 2026 20:24
loganj pushed a commit that referenced this pull request Jul 22, 2026
…ad-lifecycle

* origin/main:
  fix(desktop): strip GIF metadata extensions before upload (#2425)
  feat(desktop): gate sign-out behind key backup + typed confirmation (#2424)
  ci(desktop): add signed macOS canary build (#2419)
  feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool (#2122)
  Show team count separately in channel template rows (#2404)
  fix(dev): restore shared worktree identity from keyring (#2400)
  fix(onboarding): skip community profile setup for existing relay members (#2300)
  fix(mobile): preserve and display sidebar section icons (#2403)
  Unify sidebar chrome across themes (#2380)
  feat(media): add S3-truth per-community storage sweep (#2044)
brow pushed a commit that referenced this pull request Jul 22, 2026
…obile-releasing

Co-authored-by: npub1sv749mw8zcmld4ygjx2mx2aqcn3zvtuj2nlmgatxgad3t9uweu9q5marze <833d52edc71637f6d4889195b32ba0c4e2262f9254ffb47566475b15978ecf0a@sprout-oss.stage.blox.sqprod.co>

Signed-off-by: npub1sv749mw8zcmld4ygjx2mx2aqcn3zvtuj2nlmgatxgad3t9uweu9q5marze <833d52edc71637f6d4889195b32ba0c4e2262f9254ffb47566475b15978ecf0a@sprout-oss.stage.blox.sqprod.co>

* origin/main:
  fix(desktop): strip GIF metadata extensions before upload (#2425)
  feat(desktop): gate sign-out behind key backup + typed confirmation (#2424)
  ci(desktop): add signed macOS canary build (#2419)
  feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool (#2122)
  Show team count separately in channel template rows (#2404)
  fix(dev): restore shared worktree identity from keyring (#2400)
  fix(onboarding): skip community profile setup for existing relay members (#2300)
  fix(mobile): preserve and display sidebar section icons (#2403)
  Unify sidebar chrome across themes (#2380)
  feat(media): add S3-truth per-community storage sweep (#2044)
  feat(relay): log NIP-98 pubkey attribution on HTTP bridge requests (#2206)

Co-authored-by: npub1sv749mw8zcmld4ygjx2mx2aqcn3zvtuj2nlmgatxgad3t9uweu9q5marze <833d52edc71637f6d4889195b32ba0c4e2262f9254ffb47566475b15978ecf0a@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1sv749mw8zcmld4ygjx2mx2aqcn3zvtuj2nlmgatxgad3t9uweu9q5marze <833d52edc71637f6d4889195b32ba0c4e2262f9254ffb47566475b15978ecf0a@sprout-oss.stage.blox.sqprod.co>

# Conflicts:
#	RELEASING.md
loganj pushed a commit that referenced this pull request Jul 22, 2026
…ad-lifecycle

* origin/main:
  fix(desktop): strip GIF metadata extensions before upload (#2425)
  feat(desktop): gate sign-out behind key backup + typed confirmation (#2424)
  ci(desktop): add signed macOS canary build (#2419)
  feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool (#2122)
  Show team count separately in channel template rows (#2404)
  fix(dev): restore shared worktree identity from keyring (#2400)
  fix(onboarding): skip community profile setup for existing relay members (#2300)
  fix(mobile): preserve and display sidebar section icons (#2403)
  Unify sidebar chrome across themes (#2380)
  feat(media): add S3-truth per-community storage sweep (#2044)

Signed-off-by: npub1dpf98sl35hm9k65t8h6cvh5n6knn2msugh5k5nmysxwnw5wlh7uqn2wgp4 <685253c3f1a5f65b6a8b3df5865e93d5a7356e1c45e96a4f64819d3751dfbfb8@sprout-oss.stage.blox.sqprod.co>
lewiswang0516 added a commit to lewiswang0516/buzz that referenced this pull request Jul 23, 2026
…e canonical runtime key

Since block#2122, spawn paths passed ManagedAgentRuntimeKey.relay_url (the
canonical identity form) to spawned children as BUZZ_RELAY_URL. The
canonicalizer folds every loopback spelling to 127.0.0.1, but the relay
derives the community boundary from the Host header, so a community
configured as ws://localhost:3000 had its UI post into the localhost:3000
community while every agent connected to the 127.0.0.1:3000 community.
Agents discovered zero channels, logged 'no channel subscriptions
resolved - agent will sit idle', and silently never answered mentions.

Per the contract documented on buzz_core::relay::normalize_relay_url
('Connection code may retain the configured URL; this canonical form is
for identity'), keep the canonical key for identity, receipts, and log
paths, and dial the caller-supplied requested URL:

- spawn_agent_child dials the relay_url parameter verbatim; the runtime
  key remains identity-only
- start_managed_agent_process, start_pair, and launch restore pass their
  already-resolved requested URL instead of key.relay_url
- reconcile_managed_agent_runtimes forwards the requested community URL
  to start_pair instead of the canonical form
- probe_agent_relay_access probes over the requested URL so the HTTP
  probe hits the same community the child will join
- start_pair now reports requested_relay_url in its status rows, and the
  Active Communities settings card prefers requestedRelayUrl for
  start/stop/restart actions so a manual restart re-dials the configured
  URL rather than the canonical echo

No new automated test covers the spawn wiring itself (it forks a real
child process); verified with cargo test (1560 passed), clippy, fmt,
tsc, and biome.

Fixes block#2444

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Lewis Wang <lewis.wang@plusalsostudios.com.au>
wpfleger96 added a commit that referenced this pull request Jul 23, 2026
Rebases the gate-cleared ledger branch (715779e) onto origin/main
(4253688), which gained two commits touching buzz-acp/src/lib.rs:

- #2122: per-(agent, community) harness at GUI startup — lazy pool,
  pool_lifecycle.rs, pool_ready flag, PoolEvent::Wake arm.
- #2217: relay observer frame pacing (ObserverPublishPacer, subscribe-
  before-snapshot race fix).

Conflict resolution (lib.rs only; config.rs/pool.rs/relay.rs/queue.rs
auto-merged cleanly):

1. use std::collections: merged VecDeque (#2122) + PathBuf (branch).
2. use queue: merged pool_lifecycle::PoolLifecycle (#2122) + BatchDisposition
   (branch).
3. invalidate_channel_sessions: kept #2122's pool_ready guard AND added
   branch's ledger.invalidate_channel call.
4. relay-event dispatch_pending: kept #2122's pool_ready guard AND added
   branch's &mut ledger argument.
5. heartbeat dispatch_pending: combined #2122's pool_not_ready early-skip
   with branch's let-flushable/sync_dirty logic (extra nesting level).

Also fixed two test fixtures missing new fields (lazy_pool in test_config,
relay_url in test_prompt_context_with_dedup_mode) — both from #2122
additions that auto-merged into the non-test code.

Post-merge also threaded &mut ledger into the PoolEvent::Wake → dispatch_pending
call site (came from main's auto-merged #2122 code; had the pre-branch
signature without ledger).

Gates: cargo test -p buzz-acp (674 passed, 0 failed), cargo clippy -D warnings
(clean), just fmt-check (clean), just clippy (clean).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96 added a commit that referenced this pull request Jul 23, 2026
…elay clobber

#2122 spawns one harness per (agent, relay) pair sharing a workdir. The ledger
file was keyed by agent pubkey only; two harnesses for the same agent on
different relays would load the same file and each boot would commit a
snapshot membership-gated to its own channels, silently erasing the other
relay's pending turns.

Namespace the filename as pending-turns-<pubkey16>-<relayhash16>.json, where
relayhash16 is the first 16 hex chars of the sha256 of the canonical relay URL
(same normalization Desktop uses for runtime_id). Each (agent, relay) pair now
owns a distinct file; concurrent harnesses never share state.

Also expose a #[cfg(test)] Ledger::path() accessor so the atomic-write tests
in lib.rs can derive the correct .tmp sibling path without re-implementing the
filename logic.

Migration: pre-merge builds wrote pending-turns-<pubkey16>.json. The feature
has never shipped (PR is DRAFT), so old-format files are simply ignored on
first load — one lost recovery opportunity at worst, the same floor as a clean
install. No migration code needed.

Duplicate-writer risk: #2122 spawns at most one process per (agent, relay) key
(ManagedAgentRuntimeKey is a HashMap key and processes are keyed by it). A same-
pair duplicate writer is not possible under the current lifecycle; no lock needed.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96 added a commit that referenced this pull request Jul 27, 2026
Harnesses became per (agent, relay) pair in #2122 and now write
`agents/logs/{pubkey}__{sha256(relay_url)}.log` via
`managed_agent_runtime_log_path`. `get_managed_agent_log` was never
updated and still read the legacy `agents/logs/{pubkey}.log`, so agent
profile → Runtime → Harness Log froze at each agent's last
single-runtime line while live output accumulated in files the reader
never opened.

The reader now resolves the log through `latest_managed_agent_log_path`,
which picks the most recently modified file belonging to the agent —
pair-scoped `{pubkey}__*.log` or legacy `{pubkey}.log` — and falls back
to the legacy path when the agent has no log on disk at all. Agents that
have not restarted since the update keep working, and the panel follows
whichever harness is currently writing. The response already carried
`log_path`, so the panel header names the file being shown.

Selection is deterministic: equal mtimes break toward the higher
filename, and files belonging to other agents or without a `.log`
extension are never candidates.

`storage.rs`'s inline test module moves to a `#[path]`-included sibling
`storage_tests.rs`, matching the existing pattern in `teams.rs` and
`archive/mod.rs`. This drops both halves under the desktop file-size
limit (1383 → 826 / 701), so the ratchet entries tighten instead of
growing.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
calvadev pushed a commit to shopstr-eng/buzz that referenced this pull request Aug 3, 2026
Harnesses became per (agent, relay) pair in block#2122 and now write
`agents/logs/{pubkey}__{sha256(relay_url)}.log` via
`managed_agent_runtime_log_path`. `get_managed_agent_log` was never
updated and still read the legacy `agents/logs/{pubkey}.log`, so agent
profile → Runtime → Harness Log froze at each agent's last
single-runtime line while live output accumulated in files the reader
never opened.

The reader now resolves the log through `latest_managed_agent_log_path`,
which picks the most recently modified file belonging to the agent —
pair-scoped `{pubkey}__*.log` or legacy `{pubkey}.log` — and falls back
to the legacy path when the agent has no log on disk at all. Agents that
have not restarted since the update keep working, and the panel follows
whichever harness is currently writing. The response already carried
`log_path`, so the panel header names the file being shown.

Selection is deterministic: equal mtimes break toward the higher
filename, and files belonging to other agents or without a `.log`
extension are never candidates.

`storage.rs`'s inline test module moves to a `#[path]`-included sibling
`storage_tests.rs`, matching the existing pattern in `teams.rs` and
`archive/mod.rs`. This drops both halves under the desktop file-size
limit (1383 → 826 / 701), so the ratchet entries tighten instead of
growing.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wpfleger96 added a commit that referenced this pull request Aug 8, 2026
## Buzz Relay release v0.2.1

### Changes since relay-v0.2.0:

- fix(sdk): preserve self-mention p tags in message and forum event
builders ([#4975](#4975))
([`78c87ae20e`](78c87ae))
- feat(desktop): adding rich link previews to messages
([#3818](#3818))
([`1922d49cb2`](1922d49))
- feat(relay): accept kind:30179 private managed-agent events at ingest
([#5133](#5133))
([`ad923353a2`](ad92335))
- fix(media): require authenticated reads
([#4610](#4610))
([`769ac70b74`](769ac70))
- feat(identity): recover desktop identity from a signed-in phone
([#4845](#4845))
([`6eb65919f1`](6eb6591))
- ci: prove the relay-driven mesh lifecycle — discover, join, infer,
deny — with real nodes
([#3862](#3862))
([`38bf642fcf`](38bf642))
- relay: fuzz WebSocket 1012 restart-close timing on graceful drain
(BUZZ_DRAIN_JITTER_MS)
([#4542](#4542))
([`e14fff74d0`](e14fff7))
- fix(reactions): support max-length custom emoji
([#3833](#3833))
([`2ea9385015`](2ea9385))
- fix(channels): restrict private-channel invitations
([#4612](#4612))
([`efe1893dd3`](efe1893))
- fix(workflow): bind trigger author to the signed event
([#4607](#4607))
([`885bed35ee`](885bed3))
- fix(git): revoke access for banned relay members
([#4608](#4608))
([`997b8caaa4`](997b8ca))
- Define private managed agent wire protocol
([#4593](#4593))
([`067c085f37`](067c085))
- perf(relay): index channel-id lookups and skip trace-only reads
([#4647](#4647))
([`bc9e6528a7`](bc9e652))
- Polish mobile inbox and media flows
([#4512](#4512))
([`feccf4eabc`](feccf4e))
- fix(git): allow deleting the default branch
([#4297](#4297))
([`fc598f5f8d`](fc598f5))
- feat(projects): add buzz projects CLI commands (NIP-MP kind:30621)
([#4020](#4020))
([`b7bb15122e`](b7bb151))
- perf(relay): serve relay-membership checks from the read replica
([#4124](#4124))
([`ac4fa13b8e`](ac4fa13))
- fix(relay): allow open relays to set their NIP-11 workspace icon
(kind:9033) ([#3998](#3998))
([`5765fc74b7`](5765fc7))
- feat(relay): accept kind:30621 multi-repo projects at ingest
([#3171](#3171))
([`cb9701cd30`](cb9701c))
- feat(relay): raise hosted community limit to five
([#3829](#3829))
([`10d5a26414`](10d5a26))
- fix(relay): align NIP-11 max_limit with REQ ceiling
([#3635](#3635))
([`23f0c26b1c`](23f0c26))
- feat(relay): gate kind 30178 team-catalog reads behind the shared tag
([#3358](#3358))
([`114d40d9d3`](114d40d))
- fix(db): isolate usage metrics advisory-lock test on scratch DB
([#3670](#3670))
([`dba97eecd9`](dba97ee))
- perf(presence): reduce heartbeat frequency
([#3783](#3783))
([`bf139e8d0b`](bf139e8))
- feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute
(split 1/2 of #3467) ([#3741](#3741))
([`4933672eb4`](4933672))
- feat(replica): portable heartbeat-token fence with snapshot-local
reader routing ([#3268](#3268))
([`63496cc1d4`](63496cc))
- fix(git): channel binding tooling + author remediation for unbound
repos ([#3626](#3626))
([`788b3c002b`](788b3c0))
- feat: configure S3 URL addressing style
([#3400](#3400))
([`7012d86d52`](7012d86))
- feat(tracing): correlate trace IDs in relay logs
([#3608](#3608))
([`005b5b819a`](005b5b8))
- fix(relay): avoid subscription lock inversion
([#3413](#3413))
([`22be8bb351`](22be8bb))
- feat(cli): add users set-status command for NIP-38 profile status
([#3253](#3253))
([`60158fce3e`](60158fc))
- feat(relay): make Postgres pool size configurable, default 50
([#3191](#3191))
([`2ce2d71cc3`](2ce2d71))
- feat(tracing): add datastore tracing plumbing
([#2760](#2760))
([`e94b9aeda0`](e94b9ae))
- feat(invites): add use-limited invite links
([#3141](#3141))
([`d500c2d5cf`](d500c2d))
- feat(admin): show reported message content in report detail
([#3149](#3149))
([`f069a85503`](f069a85))
- resolve findings ([#3150](#3150))
([`9b0f744804`](9b0f744))
- Revert "fix(cli,relay): resolve agents by verified owner"
([#3168](#3168))
([`a041e2d21e`](a041e2d))
- fix(cli,relay): resolve agents by verified owner
([#2615](#2615))
([`c3084b36d9`](c3084b3))
- fix(security): enforce durable community ban on NIP-43 relay-admin
kinds 9030-9033 ([#3128](#3128))
([`e2e0079101`](e2e0079))
- fix(security): authorize kind:9000 role changes in both directions
([#3017](#3017))
([`00ecf2cac7`](00ecf2c))
- feat(desktop): handle project work from Inbox
([#3117](#3117))
([`c5c4f390b6`](c5c4f39))
- feat(relay): make per-owner community limit configurable via
BUZZ_MAX_COMMUNITIES_PER_OWNER
([#2599](#2599))
([`2a051a404d`](2a051a4))
- feat(relay): add author-only-unless-shared read gate for kind 30175
([#2768](#2768))
([`ab3af82871`](ab3af82))
- fix(core): block IPv6 transition SSRF targets
([#2801](#2801))
([`c26bf5945d`](c26bf59))
- fix(workflow): bypass system proxies for webhooks
([#2800](#2800))
([`60a171b19e`](60a171b))
- fix(audit): hash created_at at the precision Postgres stores
([#2638](#2638))
([`264a56a226`](264a56a))
- feat(desktop): make pull request reviews actionable
([#2510](#2510))
([`9081ab0ec9`](9081ab0))
- fix(relay): decompress gzip-encoded git smart-HTTP request bodies
([#2670](#2670))
([`5ca36e7b91`](5ca36e7))
- fix(sharing): preserve agent/team snapshot tEXt chunks through media
sanitization ([#2438](#2438))
([`b096b0a15a`](b096b0a))
- fix(relay): send 1012 restart close to all clients on graceful drain
([#2575](#2575))
([`1911c69aa2`](1911c69))
- fix(media): sanitize animated image uploads
([#2524](#2524))
([`8f8f5fa5a4`](8f8f5fa))
- fix(channels): strip leading hash prefixes from names
([#2250](#2250))
([`d0ab3fdb05`](d0ab3fd))
- feat(relay): make Redis pool size configurable, default 16
([#2521](#2521))
([`bcc3e13069`](bcc3e13))
- feat(desktop+acp): spawn a harness per (agent, community) pair at GUI
startup — warm sockets, lazy LLM pool
([#2122](#2122))
([`61cc738ee8`](61cc738))
- feat(media): add S3-truth per-community storage sweep
([#2044](#2044))
([`bd37a4d584`](bd37a4d))
- feat(relay): log NIP-98 pubkey attribution on HTTP bridge requests
([#2206](#2206))
([`7e34bee62c`](7e34bee))
- Revert "feat(relay): inventory unreachable Git objects"
([#2275](#2275))
([`0fb820f9bf`](0fb820f))
- feat(relay): inventory unreachable Git objects
([#2264](#2264))
([`3afc9dae15`](3afc9da))
- relay: add author_type label to buzz_events_stored_total
([#2243](#2243))
([`b9f54c43fe`](b9f54c4))
- fix(git): make project branch workflows reliable
([#2213](#2213))
([`166f27be4b`](166f27b))
- feat(cli): manage repository protection rules
([#2193](#2193))
([`f94324598d`](f943245))
- feat(cli): add agents archive/unarchive/archived subcommands
([#2173](#2173))
([`7d7992067b`](7d79920))
- fix(mobile): sanitize Android image uploads
([#2188](#2188))
([`ee21da90bd`](ee21da9))
- fix(cli): paginate channel directory queries
([#2181](#2181))
([`03fe19d603`](03fe19d))
- fix(mobile): image upload fails due to unstripped metadata
([#2185](#2185))
([`37f15b2001`](37f15b2))
- perf(relay): compact Git packs before manifest limits
([#2172](#2172))
([`80e0ab16b0`](80e0ab1))
- perf(relay): cache Git pack hydration
([#2169](#2169))
([`a4d82ec722`](a4d82ec))
- fix(relay): bound and observe Git read operations
([#2167](#2167))
([`5f7c93d9c1`](5f7c93d))
- relay: gate push enqueue on live leases; batch matcher pipeline
(T1b/T1a-repair/T2b) ([#2145](#2145))
([`e43b2d5aac`](e43b2d5))
- relay: add audit logging disable switch
([#2134](#2134))
([`bf5acabdde`](bf5acab))
- relay: skip TTL deadline bump for known-permanent channels (T1a
write-amp) ([#2125](#2125))
([`2e936d439c`](2e936d4))
- fix(git): carry NIP-OA delegation in auth event
([#2120](#2120))
([`c12257d57a`](c12257d))
- Route lag-tolerant reads to an optional Postgres read replica
([#2084](#2084))
([`29c48883d3`](29c4888))
- fix: recover community access visibility
([#2074](#2074))
([`ca384d082d`](ca384d0))
- feat: proxy feedback-scoped admin attachments
([#2059](#2059))
([`d7f918e3cb`](d7f918e))
- feat: add read-only deployment moderation dashboard
([#1999](#1999))
([`68e670e001`](68e670e))
- Bug-bash round 2: table scroll, Goose instructions, workflow mention
wake ([#2034](#2034))
([`64b8fea6dc`](64b8fea))
- Strip media metadata on clients and reject it at the relay
([#2006](#2006))
([`5cfd69cb0c`](5cfd69c))
- [codex] Hold Git concurrency permits through streaming (BUZZ-SEC-018)
([#1916](#1916))
([`7baea42abb`](7baea42))
- [codex] Enforce shared relay admission limits (BUZZ-SEC-019)
([#1917](#1917))
([`73fc0ec6cf`](73fc0ec))
- [codex] Block banned actors from moderation commands (BUZZ-SEC-007)
([#1915](#1915))
([`caa195ca58`](caa195c))
- [codex] Fix relay WebSocket admission limits
([#1682](#1682))
([`d3ce971fc7`](d3ce971))
- feat: add invite QR and mobile direct join
([#1957](#1957))
([`648cbf3610`](648cbf3))
- fix(join-policy): require legal consent on hosted invites
([#1987](#1987))
([`2e1577f76f`](2e1577f))
- [codex] Prevent actor-tag UI impersonation
([#1931](#1931))
([`c540ec9678`](c540ec9))
- Scope relay runtime state by community
([#1658](#1658))
([`d52dedb06f`](d52dedb))
- Apply optional relay join policy across join flows
([#1894](#1894))
([`6c2d667575`](6c2d667))
- feat(media): require auth for relay media reads
([#1926](#1926))
([`f308762852`](f308762))
- feat(relay): add community unarchive endpoint
([#1908](#1908))
([`6b9641db2b`](6b9641d))
- feat(relay): gate Git web GUI separately
([#1901](#1901))
([`34dc7dec75`](34dc7de))
- mesh: upgrade runtime, enforce membership, add shared compute provider
([#1656](#1656))
([`54638ff4bb`](54638ff))
- Route Git scratch through configured volume
([#1884](#1884))
([`2318b3096c`](2318b30))
- feat(relay): gate usage metrics behind stable leader
([#1814](#1814))
([`59e9821503`](59e9821))
- Relay mesh: cross-pod tunnel + huddle transport (buzz-relay-mesh)
([#1670](#1670))
([`ccb021d713`](ccb021d))
- feat(push): deliver accepted relay events as wakes
([#1866](#1866))
([`bffbc5f22c`](bffbc5f))
- fix(db): resolve duplicate migration version
([#1863](#1863))
([`08ad38a07f`](08ad38a))
- Add private product feedback sidecar
([#1857](#1857))
([`af190c93e1`](af190c9))
- feat(relay): add durable community archival
([#1834](#1834))
([`2b15a72675`](2b15a72))
- feat(push): add public APNs gateway
([#1770](#1770))
([`1c006822e4`](1c00682))
- feat(relay): add atomic community ownership transfer
([#1845](#1845))
([`52e42ccb9f`](52e42cc))
- Bound NIP-RS retention and search indexing
([#1771](#1771))
([`1b4703021d`](1b47030))
- Add optional standalone pairing relay to Helm chart
([#1799](#1799))
([`9b47c8548f`](9b47c85))
- fix(relay): publish membership snapshot on provisioning
([#1761](#1761))
([`0950d392b7`](0950d39))
- feat(relay): per-community usage metrics
([#1723](#1723))
([`620822899a`](6208228))
- refactor(desktop): remove vestigial MCP toolsets config
([#1776](#1776))
([`dfec75b3c0`](dfec75b))

**To release:** merge this PR. The tag and build will happen
automatically.

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
yjc801 added a commit to yjc801/buzz that referenced this pull request Aug 9, 2026
Managed agents live in one global store shared by every community, so
every picker offered every record — including several identically-named
identities provisioned separately per community (three "Bumble"s,
distinguishable only by npub). Only one has a harness in the community
being viewed; mentioning either of the others is silently dead, because
nothing fails: the p-tag is delivered to a relay where that pubkey was
never started.

Model (matches Slack/GitHub/Discord: global definition, per-tenant
installed identity): a persona stays global; a managed agent instance
now belongs to a community. New `community_relay_url` on the record
(canonical relay URL; null = unscoped/shared, offered everywhere).
Display and name-uniqueness scope ONLY — spawn resolution
(`effective_agent_relay_url`, agents-everywhere block#2122) is untouched, so
the change is rollback-safe.

- Field: `#[serde(default)]`, deliberately NO `skip_serializing_if` —
  the boot backfill is idempotent by key presence, so an explicit
  unscope must persist a literal null or the next boot would re-bind it
  from the legacy pin.
- Boot backfill (migration/community_scope.rs): derives the binding
  from the legacy creation-era `relay_url` pin when present; blank pin
  carries no evidence and stays unscoped. Verified byte-stable on a
  second run against a copy of a live store.
- Creation stamps the active community at all three minting sites and
  enforces per-(community, name) uniqueness inside the store lock
  (case-insensitive, instances only, unscoped collides everywhere;
  create-time only — pre-existing duplicates keep working). The create
  dialog preflights the same rule client-side so a collision rejects
  inline instead of surfacing after the persona exists.
- Pickers scope through `managedAgentBelongsToCommunity` (directory
  presence kept as an override: an agent registered here is mentionable
  here regardless of its binding): mentions, new-message recipients,
  members sidebar, projects prompt, Pulse, and the Agents page — which
  partitions into in-community + a collapsed "From other communities"
  group. Resolution of already-present pubkeys (history, membership,
  runtime, search authors, tray) stays deliberately unfiltered.
- New `set_managed_agent_community` command + `AgentCommunityScopeBadge`
  (badge + menu on agent cards/rows): assign to the active community,
  move, or share across all communities.

File-size ratchet extractions along the way: migration/json_patch.rs,
managed_agents/{community_scope,record_views}.rs,
commands/agent_create_support.rs, shared/api/{managedAgent,
managedAgentRaw}.ts, messages/lib/managedAgentMentionMaps.ts, and two
exhaustive Rust test fixtures converted to the JSON-fixture convention.

Verified: cargo test 2283 passed; desktop tests 4562 passed; tsc,
clippy -D warnings, cargo fmt, pnpm check, git diff --check all clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Junchao Yan <yjc801@gmail.com>
yjc801 added a commit to yjc801/buzz that referenced this pull request Aug 9, 2026
Managed agents live in one global store shared by every community, so
every picker offered every record — including several identically-named
identities provisioned separately per community (three "Bumble"s,
distinguishable only by npub). Only one has a harness in the community
being viewed; mentioning either of the others is silently dead, because
nothing fails: the p-tag is delivered to a relay where that pubkey was
never started.

Model (matches Slack/GitHub/Discord: global definition, per-tenant
installed identity): a persona stays global; a managed agent instance
now belongs to a community. New `community_relay_url` on the record
(canonical relay URL; null = unscoped/shared, offered everywhere).
Display and name-uniqueness scope ONLY — spawn resolution
(`effective_agent_relay_url`, agents-everywhere block#2122) is untouched, so
the change is rollback-safe.

- Field: `#[serde(default)]`, deliberately NO `skip_serializing_if` —
  the boot backfill is idempotent by key presence, so an explicit
  unscope must persist a literal null or the next boot would re-bind it
  from the legacy pin.
- Boot backfill (migration/community_scope.rs): derives the binding
  from the legacy creation-era `relay_url` pin when present; blank pin
  carries no evidence and stays unscoped. Verified byte-stable on a
  second run against a copy of a live store.
- Creation stamps the active community at all three minting sites and
  enforces per-(community, name) uniqueness inside the store lock
  (case-insensitive, instances only, unscoped collides everywhere;
  create-time only — pre-existing duplicates keep working). The create
  dialog preflights the same rule client-side so a collision rejects
  inline instead of surfacing after the persona exists.
- Pickers scope through `managedAgentBelongsToCommunity` (directory
  presence kept as an override: an agent registered here is mentionable
  here regardless of its binding): mentions, new-message recipients,
  members sidebar, projects prompt, Pulse, and the Agents page — which
  partitions into in-community + a collapsed "From other communities"
  group. Resolution of already-present pubkeys (history, membership,
  runtime, search authors, tray) stays deliberately unfiltered.
- New `set_managed_agent_community` command + `AgentCommunityScopeBadge`
  (badge + menu on agent cards/rows): assign to the active community,
  move, or share across all communities.

File-size ratchet extractions along the way: migration/json_patch.rs,
managed_agents/{community_scope,record_views}.rs,
commands/agent_create_support.rs, shared/api/{managedAgent,
managedAgentRaw}.ts, messages/lib/managedAgentMentionMaps.ts, and two
exhaustive Rust test fixtures converted to the JSON-fixture convention.

Verified: cargo test 2283 passed; desktop tests 4562 passed; tsc,
clippy -D warnings, cargo fmt, pnpm check, git diff --check all clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Junchao Yan <yjc801@gmail.com>
yjc801 added a commit to yjc801/buzz that referenced this pull request Aug 9, 2026
* feat(desktop): scope managed agents per community

Managed agents live in one global store shared by every community, so
every picker offered every record — including several identically-named
identities provisioned separately per community (three "Bumble"s,
distinguishable only by npub). Only one has a harness in the community
being viewed; mentioning either of the others is silently dead, because
nothing fails: the p-tag is delivered to a relay where that pubkey was
never started.

Model (matches Slack/GitHub/Discord: global definition, per-tenant
installed identity): a persona stays global; a managed agent instance
now belongs to a community. New `community_relay_url` on the record
(canonical relay URL; null = unscoped/shared, offered everywhere).
Display and name-uniqueness scope ONLY — spawn resolution
(`effective_agent_relay_url`, agents-everywhere block#2122) is untouched, so
the change is rollback-safe.

- Field: `#[serde(default)]`, deliberately NO `skip_serializing_if` —
  the boot backfill is idempotent by key presence, so an explicit
  unscope must persist a literal null or the next boot would re-bind it
  from the legacy pin.
- Boot backfill (migration/community_scope.rs): derives the binding
  from the legacy creation-era `relay_url` pin when present; blank pin
  carries no evidence and stays unscoped. Verified byte-stable on a
  second run against a copy of a live store.
- Creation stamps the active community at all three minting sites and
  enforces per-(community, name) uniqueness inside the store lock
  (case-insensitive, instances only, unscoped collides everywhere;
  create-time only — pre-existing duplicates keep working). The create
  dialog preflights the same rule client-side so a collision rejects
  inline instead of surfacing after the persona exists.
- Pickers scope through `managedAgentBelongsToCommunity` (directory
  presence kept as an override: an agent registered here is mentionable
  here regardless of its binding): mentions, new-message recipients,
  members sidebar, projects prompt, Pulse, and the Agents page — which
  partitions into in-community + a collapsed "From other communities"
  group. Resolution of already-present pubkeys (history, membership,
  runtime, search authors, tray) stays deliberately unfiltered.
- New `set_managed_agent_community` command + `AgentCommunityScopeBadge`
  (badge + menu on agent cards/rows): assign to the active community,
  move, or share across all communities.

File-size ratchet extractions along the way: migration/json_patch.rs,
managed_agents/{community_scope,record_views}.rs,
commands/agent_create_support.rs, shared/api/{managedAgent,
managedAgentRaw}.ts, messages/lib/managedAgentMentionMaps.ts, and two
exhaustive Rust test fixtures converted to the JSON-fixture convention.

Verified: cargo test 2283 passed; desktop tests 4562 passed; tsc,
clippy -D warnings, cargo fmt, pnpm check, git diff --check all clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Junchao Yan <yjc801@gmail.com>

* fix(desktop): scope agent reuse and Welcome selection to the community

Addresses review findings 1, 2 and 4.

Finding 1 — backfill scope before typed store rewrites. `community_relay_url`
is `#[serde(default)]` with no `skip_serializing_if`, and the persona fold
deserializes and re-serializes the whole managed-agent store, so it turns
"key absent" into an explicit `null`. Running the backfill after the fold
therefore stranded every pre-existing instance on any upgrade that still had
personas.json: the key-presence idempotency guard read that null as a
deliberate unscope and never derived the binding from the non-empty legacy
pin. The backfill now runs before the first typed rewrite; the steps ahead of
it are raw JSON patches, so key absence still survives to that point, and
records added later serialize their own explicit null, which is the correct
unscoped binding for a definition. Corrects the fold's doc comment, which
claimed instance records pass through byte-identical.

Finding 2 — restrict automatic reuse to the active community. The reuse
selectors filtered only on persona/command and channel membership, so
provisioning a persona in community B attached and started the instance minted
for A instead of minting B's own; the preset path matched on a deliberately
generic runtime-id name with the same gap. Reuse is now gated on
`managedAgentIsReusableInCommunity`, threaded as a required argument through
every selector so no caller can omit it. That rule is deliberately stricter
than the picker's visibility predicate: directory presence proves an identity
runs here, which is the right signal for showing a row and the wrong one for
adopting a record. Unscoped instances stay shared; an unresolved community
fails closed, because minting a duplicate is recoverable and adopting another
community's identity is the defect. An agent already in the channel is an
established binding rather than a fresh adoption, so it stays unscoped.

Finding 4 — use community scope for Welcome agent selection. Both selectors
compared the legacy `relayUrl` creation pin, which a move deliberately leaves
untouched, so a moved Welcome agent was invisible in the community it now
belongs to — its replacement create then hit the new scoped name collision
rule — and remained selectable in the one it left. Both now route through
`managedAgentBelongsToCommunity`. The existing relay-scoped tests expressed
the right intent through the wrong field and were moved onto
`communityRelayUrl`.

Signed-off-by: Junchao Yan <yjc801@gmail.com>

* fix(desktop): enforce scoped name uniqueness on snapshot imports

Addresses review finding 3.

Both import paths stamped the active community scope onto minted instances
but checked only for duplicate pubkeys under the store lock, so importing a
snapshot named Bumble into a community that already offers Bumble persisted a
second same-name scoped instance — recreating exactly the ambiguous picker
entry this scoping exists to remove.

Persona snapshot import now resolves its scope through
`mint_scope_and_check_name`, the same helper `create_managed_agent` uses, so
the check and the write share one critical section.

Team snapshot import checks every member inside the lock, accumulating the
members as it goes so they are validated against one another as well as
against the existing store: one snapshot can carry two members that collide
with each other while neither collides with anything already stored.

Signed-off-by: Junchao Yan <yjc801@gmail.com>

* fix(desktop): carry community scope through batch provisioning

Addresses round-2 review findings 1, 2 and 3.

1. Thread the active community through batch provisioning.
`createChannelManagedAgents` built its reuse context without
`activeCommunityRelayUrl`, so every batch caller — add-team/add-bot dialogs,
quick bot drop, mention-send, template application — handed the new reuse
predicate `undefined`. A record bound to the CURRENT community then failed
closed, was skipped, and provisioning fell through to `createManagedAgent`,
where the new scoped name-uniqueness gate rejected the replacement: ordinary
same-community reuse broken in the name of blocking cross-community adoption.

The scope is now a required argument rather than an optional context field.
`ChannelAgentCommunityContext` (`{ activeCommunityRelayUrl: string | null }`)
is required by `createChannelManagedAgents` and
`ensureChannelAgentPresetInChannel`; `ChannelAgentProvisionContext` extends it
with the batch-shared `managedAgents` / `channelMemberPubkeys` and is required
by `provisionChannelManagedAgent` and `createChannelManagedAgent`. A new call
site cannot omit the scope without a type error, and `null` stays the explicit
"unresolved community" value the reuse predicate fails closed on. Making the
reuse inputs non-optional also retires the two `context?.managedAgents &&
context.channelMemberPubkeys` guards, which no caller could trip.

Production callers updated: `useCreateChannelManagedAgentMutation`,
`useCreateChannelManagedAgentsMutation`, and `useApplyTemplate` now read
`useActiveCommunityRelayUrl()` and pass it down.

2. Restore the snapshot importer below the repository size ratchet.
The uniqueness fix pushed `snapshot/import.rs` to 1,010 lines against a 1,000
ceiling, failing `check:file-sizes`. Its two inline `#[cfg(test)]` modules move
verbatim to a sibling `snapshot/import_tests.rs`, matching the module's
existing `snapshot/tests_*.rs` convention; only the two `use` paths changed
(`super::` -> `super::super::`). import.rs is now 874 lines.

3. Run rustfmt on the team import change.
`use crate::managed_agents::{...}` in `commands/team_snapshot.rs` had
`AgentDefinition` split onto its own line; the three type names fit one line.

Signed-off-by: Junchao Yan <yjc801@gmail.com>

* fix(desktop): keep the boundary-7 egress test in the inventoried file

Addresses round-3 review findings 1 and 2.

1. Preserve the security inventories when moving snapshot tests.
Splitting BOTH inline test modules out of `snapshot/import.rs` was not the pure
move I claimed: `egress_guard_tests.rs` scans source files and pins two
inventories by path. `EVENTS_INVENTORY` attributes 2 egress-URL sites to
import.rs (boundary 7 plus its in-file injection fixture), and the key-backup
source allowlist lists import.rs by name. Relocating the fixture dropped
import.rs to 1 site and put the fixture in a file on neither list, so
`events_url_inventory_is_fully_guarded` and
`ncryptsec_handling_is_confined_to_allowlisted_files` both failed.

`egress_guard_tests` moves back inline, verbatim. The avatar tests alone carry
the line-count relief (import.rs is 907 lines against the 1,000 ceiling), so
nothing is gained by relocating the fixture — and the alternative, widening two
security inventories to buy line count, is the wrong trade. `egress_guard_tests.rs`
already documents this module as boundary 7's injection test by path, and that
statement is true again. The sibling file's own header records why only the
avatar tests live there, and it names no scanner needle of its own.

2. Apply rustfmt's actual wrapping.
`save_teams` moves to the following line in the `crate::managed_agents` use
group, per the reviewer's `cargo fmt --check` output. My previous rewrap was
computed by hand rather than observed; the width arithmetic put that line at
exactly 100 columns, and rustfmt does not accept it there.

Signed-off-by: Junchao Yan <yjc801@gmail.com>

* fix(desktop): extract channel-agent mutations to clear the size ratchet

Rebasing onto main put `hooks.ts` at 994 lines before this branch touches
it, so the four `useActiveCommunityRelayUrl()` reads that thread community
scope through the batch provisioning hooks pushed it to 1006 — over the
1000-line ceiling `check:file-sizes` enforces.

Moves the five channel-scoped provisioning mutations (attach, ensure
preset, create one, provision, create batch) to a sibling
`channelAgentMutations.ts`. That is exactly the cluster this branch grew,
and it is the only user of `invalidateAgentQueriesInBackground` and
`isCachedDmChannel`, which are now exported from `hooks.ts` rather than
moved: the new module imports one-directionally, matching how
`teamHooks.ts` sits under `hooks.ts` with no cycle.

Consumers import the five hooks from the new path directly instead of
re-exporting through `hooks.ts`, which would have made the dependency
circular. `hooks.ts` is 781 lines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Junchao Yan <yjc801@gmail.com>

---------

Signed-off-by: Junchao Yan <yjc801@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants